9|点光源和聚光灯阴影

loading

9.1 聚光灯阴影

现在我们开始支持聚光灯的实时阴影,使用的方法和方向光大致相同,不过需要进行一些修改。我们还将使用阴影图集的方式,按照Unity提供的顺序填充阴影图块。

9.1.1 阴影混合

1. 首先定义一个新的方法GetOtherShadow用于计算非定向光源的阴影衰减。然后使用GetOtherShadowAttenuation方法,这个和GetDirectionalShadowAttenuation方法类似,调用新方法得到实时阴影后和调用烘焙阴影进行混合。

其中全局的阴影强度用来确定是否可以跳过实时阴影的采样,可能因为我们超出了最大阴影距离或在级联包围球之外。虽然级联阴影仅适用于方向光阴影,它对于其它类型的光源没有意义,这些光具有固定位置,它们的阴影贴图不会随着视图移动而改变,但我们还是以同样的方式来过渡阴影,因为可能屏幕中的某些区域没有方向光阴影,但确实有非定向光源的阴影,所以我们对所有对象使用相同的全局阴影强度。

//得到非定向光源的实时阴影衰减
float GetOtherShadow (OtherShadowData other, ShadowData global, Surface surfaceWS)
{
return 1.0;
}

//得到非定向光源的阴影衰减
float GetOtherShadowAttenuation(OtherShadowData other, ShadowData global, Surface surfaceWS)
{
...
if (other.strength * global.strength <= 0.0)
{
shadow = GetBakedShadow(global.shadowMask, other.shadowMaskChannel, abs(other.strength));
}
else
{
shadow = GetOtherShadow(other, global, surfaceWS);
shadow = MixBakedAndRealtimeShadows(global, shadow, other.shadowMaskChannel, other.strength);
}
return shadow;
}

2. 现在还必须处理一个极端情况,当不存在方向光阴影的时候确实存在非定向光源的阴影,这种情况发生时,没有任何级联,因此它们不应该影响全局阴影强度,但仍然需要阴影距离过渡值,因此将Shadows.RenderDirectionShadows方法中传递级联计数和阴影距离过渡数据的代码移动到Render方法中,并在适当的时候将级联计数设置为0。

public void Render()
{
...
//将级联计数发送到GPU
buffer.SetGlobalInt(cascadeCountId, ShadowedDirectionalLightCount > 0 ? settings.directional.cascadeCount : 0);
//阴影距离过渡相关数据发送GPU
float f = 1f - settings.directional.cascadeFade;
buffer.SetGlobalVector(shadowDistanceFadeId, new Vector4(1f / settings.maxDistance, 1f / settings.distanceFade, 1f / (1f - f * f)));

buffer.EndSample(bufferName);
ExecuteBuffer();
}

void RenderDirectionalShadows()
{
...
//将级联计数发送到GPU
//buffer.SetGlobalInt(cascadeCountId, settings.directional.cascadeCount);
buffer.SetGlobalVectorArray(cascadeCullingSpheresId, cascadeCullingSpheres);

buffer.SetGlobalVectorArray(cascadeDataId, cascadeData);

buffer.SetGlobalMatrixArray(dirShadowMatricesId, dirShadowMatrices);
//阴影距离过渡相关数据发送GPU
//float f = 1f - settings.directional.cascadeFade;
//buffer.SetGlobalVector(shadowDistanceFadeId,new Vector4(1f / settings.maxDistance, 1f / settings.distanceFade,1f / (1f - f * f)));

...
}

3. 最后确保Shadows.hlsl的GetShadowData方法中,超出最大级联范围且级联数量大于0时才把全局强度设为0。

    //如果超出最大级联范围且级联数量大于0,将全局阴影强度设为0(不进行阴影采样)  
if (i == _CascadeCount && _CascadeCount > 0)
{
data.strength = 0.0;
}

9.1.2 非定向光源的实时阴影

1. 方向光阴影有自己的阴影图集,我们对非定向光源的阴影使用单独的阴影图集,并分别计数。将可以产生阴影的非定向光源最大数量限制为最大16个,在Shadows脚本中定义相关字段和初始化操作。

    //可投射阴影的非定向光源最大数量
const int maxShadowedOtherLightCount = 16;
//已存在的可投射阴影的非定向光数量
int shadowedOtherLightCount;

public void Setup(ScriptableRenderContext context, CullingResults cullingResults,ShadowSettings settings)
{
...
shadowedOtherLightCount = 0;
}

2. 这意味着我们最终可以使用开启了阴影但是无法渲染到阴影图集中的光源,哪些光源不会产生阴影取决于它们在可见光列表中的位置。我们不会为在列表外的光源保留阴影,除非它们烘焙了阴影。为此我们调整ReserveOtherShadows方法,开头判断灯光如果没有阴影直接返回,否则检测阴影蒙版贴图通道,始终返回阴影强度和通道。

    public Vector4 ReserveOtherShadows(Light light, int visibleLightIndex)
{
if (light.shadows == LightShadows.None || light.shadowStrength <= 0f)
{
return new Vector4(0f, 0f, 0f, -1f);
}
float maskChannel = -1f;
LightBakingOutput lightBaking = light.bakingOutput;
if (lightBaking.lightmapBakeType == LightmapBakeType.Mixed && lightBaking.mixedLightingMode == MixedLightingMode.Shadowmask)
{
useShadowMask = true;
maskChannel = lightBaking.occlusionMaskChannel;

}
return new Vector4(light.shadowStrength, 0f, 0f, maskChannel);
}

3. 然后在返回之前检查一下非定向光源数量是否超过了设置的最大数量或者是否没有阴影需要渲染,如果为true,则返回负的阴影强度值和Mask通道,否则继续增加非定向光源的计数并设置图块索引。

if (shadowedOtherLightCount >= maxShadowedOtherLightCount ||!cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b) )
{
return new Vector4(-light.shadowStrength, 0f, 0f, maskChannel);
}

return new Vector4(light.shadowStrength, shadowedOtherLightCount++, 0f, maskChannel);

9.1.3 非定向光源的阴影图集

1. 定向光阴影和非定向光阴影是分开的,所以我们在ShadowSettings脚本中进行单独的配置,这里无需设置级联,只需要包含图集大小和滤波模式。

    //非定向光源的阴影图集设置
[System.Serializable]
public struct Other
{

public TextureSize atlasSize;

public FilterMode filter;
}

public Other other = new Other
{
atlasSize = TextureSize._1024,
filter = FilterMode.PCF2x2
};

loading

2. 在Lit.shader的CustomLit Pass块中添加一条多编译指令,用来设置非定向光源阴影的滤波模式。

#pragma multi_compile _ _OTHER_PCF3 _OTHER_PCF5 _OTHER_PCF7

3. 接下来在Shadows脚本中添加相应的关键字数组,阴影图集和阴影转换矩阵的着色器标识ID和一个用于存储转换矩阵的数组。

    //非定向光源的滤波模式
static string[] otherFilterKeywords =
{
"_OTHER_PCF3",
"_OTHER_PCF5",
"_OTHER_PCF7",
};

static int otherShadowAtlasId = Shader.PropertyToID("_OtherShadowAtlas");
static int otherShadowMatricesId = Shader.PropertyToID("_OtherShadowMatrices");

static Matrix4x4[] otherShadowMatrices = new Matrix4x4[maxShadowedOtherLightCount];

4. 之前我们已经使用一个向量的XY分量将定向光源的阴影图集大小和纹素大小发送到GPU,现在我们还需要发送非定向光源的阴影图集大小和纹素大小,可以将它们放入同一向量的ZW分量中,于是我们把该向量定义在外部作为一个全局向量,并将发生图集大小的操作转移到Render方法中,RenderDirectionalShadows方法中只需要给该向量的XY分量赋值。

Vector4 atlasSizes;
public void Render()
{
...
//传递图集大小和纹素大小
buffer.SetGlobalVector(shadowAtlasSizeId, atlasSizes);
buffer.EndSample(bufferName);
ExecuteBuffer();
}
void RenderDirectionalShadows()
{
//创建renderTexture
int atlasSize = (int)settings.directional.atlasSize;
atlasSizes.x = atlasSize;
atlasSizes.y = 1f / atlasSize;

...
//传递图集大小和纹素大小
//buffer.SetGlobalVector( shadowAtlasSizeId, new Vector4(atlasSize, 1f / atlasSize));

buffer.EndSample(bufferName);
ExecuteBuffer();
}

5. 之后复制RenderDirectionalShadows方法并命名为RenderOtherShadows,只需要做一些修改即可。设置正确的图集和转换矩阵,图集大小和纹素存储在全局向量的ZW分量中。然后删除循环中传递级联包围球数据的代码,也删除渲染定向光阴影的调用,最终结构如下:

    //渲染非定向光阴影
void RenderOtherShadows()
{
//创建renderTexture
int atlasSize = (int)settings.other.atlasSize;
atlasSizes.z = atlasSize;
atlasSizes.w = 1f / atlasSize;

buffer.GetTemporaryRT(otherShadowAtlasId, atlasSize, atlasSize, 32, FilterMode.Bilinear, RenderTextureFormat.Shadowmap);
//指定渲染的阴影数据存储到RT中
buffer.SetRenderTarget(otherShadowAtlasId, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
//清除深度缓冲区
buffer.ClearRenderTarget(true, false, Color.clear);

buffer.BeginSample(bufferName);
ExecuteBuffer();
//要分割的图块数量和大小
int tiles = shadowedOtherLightCount;
int split = tiles <= 1 ? 1 : tiles <= 4 ? 2 : 4;
int tileSize = atlasSize / split;
//遍历所有光源渲染阴影贴图
for (int i = 0; i < shadowedOtherLightCount; i++)
{
//RenderDirectionalShadows(i, split, tileSize);
}

//阴影转换矩阵传入GPU
buffer.SetGlobalMatrixArray(otherShadowMatricesId, otherShadowMatrices);
SetKeywords(otherFilterKeywords, (int)settings.other.filter - 1);

buffer.EndSample(bufferName);
ExecuteBuffer();
}

6. 当可投影的非定向光源数量大于0时进行阴影的渲染,否则也需要提供一个虚拟纹理,就像定向光阴影一样,我们可以简单地使用定向光的阴影图集,并在Cleanup方法中进行释放申请的RT内存。

    public void Render()
{
if (ShadowedDirectionalLightCount > 0)
{
RenderDirectionalShadows();
}
else
{
buffer.GetTemporaryRT(dirShadowAtlasId, 1, 1,32, FilterMode.Bilinear, RenderTextureFormat.Shadowmap);
}
if (shadowedOtherLightCount > 0)
{
RenderOtherShadows();
}
else
{
buffer.SetGlobalTexture(otherShadowAtlasId, dirShadowAtlasId);
}
...
}

//释放创建的RT内存
public void Cleanup()
{
buffer.ReleaseTemporaryRT(dirShadowAtlasId);
if (shadowedOtherLightCount > 0)
{
buffer.ReleaseTemporaryRT(otherShadowAtlasId);
}
ExecuteBuffer();
}

9.1.4 渲染聚光灯阴影

1. 要渲染聚光灯阴影,我们需要知道可见光的索引,斜度比例偏差和法线偏差。在Shadows脚本中我们为这些字段创建一个ShadowedOtherLight结构体,并为其添加一个数组进行相关数据的存储。然后在ReserveOtherShadows方法中返回之前存储的相关数据。

    struct ShadowedOtherLight
{
public int visibleLightIndex;
public float slopeScaleBias;
public float normalBias;
}
//存储可投射阴影的非定向光源的数据
ShadowedOtherLight[] shadowedOtherLights = new ShadowedOtherLight[maxShadowedOtherLightCount];

public Vector4 ReserveOtherShadows(Light light, int visibleLightIndex)
{
...
shadowedOtherLights[shadowedOtherLightCount] = new ShadowedOtherLight
{
visibleLightIndex = visibleLightIndex,
slopeScaleBias = light.shadowBias,
normalBias = light.shadowNormalBias
};
return new Vector4(light.shadowStrength, shadowedOtherLightCount++, 0f, maskChannel);
}

2. 但现在我们不能保证发送到ReserveOtherShadows方法中的灯光索引是正确的,因为它会将自己的索引传递给其它光源,当有方向光阴影时,这样索引是错误的。在Lighting脚本中,我们对这些光源添加正确的可见光索引参数来解决这个问题,并在存储阴影数据时使用该参数。为了保证一致,定向光也进行该操作。

 
void SetupDirectionalLight(int index, int visibleIndex, ref VisibleLight visibleLight)
{
...
dirLightShadowData[index] = shadows.ReserveDirectionalShadows(visibleLight.light,visibleIndex);
}

void SetupPointLight(int index, int visibleIndex, ref VisibleLight visibleLight)
{
...
otherLightShadowData[index] = shadows.ReserveOtherShadows(light, visibleIndex);
}

void SetupSpotLight(int index, int visibleIndex, ref VisibleLight visibleLight)
{
...
otherLightShadowData[index] = shadows.ReserveOtherShadows(light, visibleIndex);
}

3. 在SetupLights方法中将可见光的索引传递给对应光源类型的Setup方法。

switch (visibleLight.lightType)
{
case LightType.Directional:
if (dirLightCount < maxDirLightCount)
{
SetupDirectionalLight(dirLightCount++, i, ref visibleLight);
}
break;
case LightType.Point:
if (otherLightCount < maxOtherLightCount)
{
newIndex = otherLightCount;
SetupPointLight(otherLightCount++, i, ref visibleLight);
}
break;
case LightType.Spot:
if (otherLightCount < maxOtherLightCount)
{
newIndex = otherLightCount;
SetupSpotLight(otherLightCount++, i, ref visibleLight);
}
break;
}

4. 在Shadows脚本中创建RenderSpotShadows方法渲染聚光灯阴影,该方法和带有参数的RenderDirectionalShadows方法基本相同,只是它不会在多个阴影图块上循环,也没有级联和剔除因子。这里我们使用CullingResults的ComputeSpotShadowMatricesAndCullingPrimitives方法,该方法工作原理和ComputeDirectionalShadowMatricesAndCullingPrimitives类似,只不过它只有可见光索引、转换矩阵和拆分数据作为参数。

    //渲染聚光灯阴影
void RenderSpotShadows(int index, int split, int tileSize)
{
ShadowedOtherLight light = shadowedOtherLights[index];
var shadowSettings = new ShadowDrawingSettings(cullingResults, light.visibleLightIndex);
cullingResults.ComputeSpotShadowMatricesAndCullingPrimitives(light.visibleLightIndex, out Matrix4x4 viewMatrix,out Matrix4x4 projectionMatrix, out ShadowSplitData splitData);
shadowSettings.splitData = splitData;
otherShadowMatrices[index] = ConvertToAtlasMatrix(projectionMatrix * viewMatrix,SetTileViewport(index, split, tileSize), split);
//设置视图投影矩阵
buffer.SetViewProjectionMatrices(viewMatrix, projectionMatrix);
//设置斜度比例偏差值
buffer.SetGlobalDepthBias(0f, light.slopeScaleBias);
//绘制阴影
ExecuteBuffer();
context.DrawShadows(ref shadowSettings);
buffer.SetGlobalDepthBias(0f, 0f);
}

5. 在RenderOtherShadows方法中循环所有可投影非定向光源时调用该方法。

for (int i = 0; i < shadowedOtherLightCount; i++)
{
RenderSpotShadows(i, split, tileSize);
}

9.1.5 没有Shadow Pancaking(阴影平坠)

目前我们使用的是和方向光相同的ShadowCaster Pass来渲染聚光灯阴影,现在效果是正常的,但是Shadow Pancaking(阴影平坠)只适用于正交投影,用于假定无限远的方向光。聚光灯有实际的位置坐标,阴影投射可能部分区域位于光源位置的后面,在这种情况下使用透视投影时,将顶点Clamp到近平面会严重扭曲这些阴影,因此当Pancaking未启用时应关闭Clamp。

1. 首先在Shadows脚本中定义一个shadowPancaking的着色器标识ID,在RenderDirectionalShadows方法中渲染阴影之前将shadowPancaking设置为1,在RenderOtherShadows方法中设置为0,然后该值发送到GPU。

   static int shadowPancakingId = Shader.PropertyToID("_ShadowPancaking");
void RenderDirectionalShadows()
{
...
buffer.ClearRenderTarget(true, false, Color.clear);
buffer.SetGlobalFloat(shadowPancakingId, 1f);
buffer.BeginSample(bufferName);
...
}
void RenderOtherShadows()
{
...
buffer.ClearRenderTarget(true, false, Color.clear);
buffer.SetGlobalFloat(shadowPancakingId, 0f);
buffer.BeginSample(bufferName);
...
}

2. 在ShadowCasterPass.hlsl中定义一个bool值判断是否启用了ShadowPancaking,在启用时才对顶点进行Clamp。

bool _ShadowPancaking;
//顶点函数
Varyings ShadowCasterPassVertex(Attributes input)
{
...
if (_ShadowPancaking)
{
#if UNITY_REVERSED_Z
output.positionCS.z = min(output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE);
#else
output.positionCS.z = max(output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE);
#endif
}

//计算缩放和偏移后的UV坐标
output.baseUV = TransformBaseUV(input.baseUV);
return output;
}

9.1.6 采样聚光灯阴影

1. 要对非定向光源阴影进行采样,要对Shadows.hlsl文件进行调整,首先定义非定向光源要使用的滤波模式相关宏、可投影的最大光源数、阴影图集和阴影转换矩阵数组。

#if defined(_OTHER_PCF3)
#define OTHER_FILTER_SAMPLES 4
#define OTHER_FILTER_SETUP SampleShadow_ComputeSamples_Tent_3x3
#elif defined(_OTHER_PCF5)
#define OTHER_FILTER_SAMPLES 9
#define OTHER_FILTER_SETUP SampleShadow_ComputeSamples_Tent_5x5
#elif defined(_OTHER_PCF7)
#define OTHER_FILTER_SAMPLES 16
#define OTHER_FILTER_SETUP SampleShadow_ComputeSamples_Tent_7x7
#endif

#define MAX_SHADOWED_OTHER_LIGHT_COUNT 16

TEXTURE2D_SHADOW(_OtherShadowAtlas);


CBUFFER_START(_CustomShadows)
...
float4x4 _OtherShadowMatrices[MAX_SHADOWED_OTHER_LIGHT_COUNT];
CBUFFER_END

2. 复制SampleDirectionalShadowAtlas方法和FilterDirectionalShadow方法,命名为SampleOtherShadowAtlas和FilterOtherShadow。调整一些代码使其作用于非定向光源阴影的采样,需要注意的是我们使用_ShadowAtlasSize的ZW分量拿到图集大小。

float SampleOtherShadowAtlas (float3 positionSTS) 
{
return SAMPLE_TEXTURE2D_SHADOW(_OtherShadowAtlas, SHADOW_SAMPLER, positionSTS);
}

float FilterOtherShadow (float3 positionSTS)
{
#if defined(OTHER_FILTER_SETUP)
//样本权重
real weights[OTHER_FILTER_SAMPLES];
//样本位置
real2 positions[OTHER_FILTER_SAMPLES];
float4 size = _ShadowAtlasSize.wwzz;
OTHER_FILTER_SETUP(size, positionSTS.xy, weights, positions);
float shadow = 0;
for (int i = 0; i < OTHER_FILTER_SAMPLES; i++)
{
//遍历所有样本得到权重和
shadow += weights[i] * SampleOtherShadowAtlas(float3(positions[i].xy, positionSTS.z));
}
return shadow;
#else
return SampleOtherShadowAtlas(positionSTS);
#endif
}

3. 在OtherShadowData结构体中声明一个图块索引属性。

struct OtherShadowData 
{
float strength;
int tileIndex;
int shadowMaskChannel;
};

4. 在Light.hlsl的GetOtherShadowData方法中获取图块索引。

//获取非定向光源的阴影数据
OtherShadowData GetOtherShadowData(int lightIndex)
{
OtherShadowData data;
data.strength = _OtherLightShadowData[lightIndex].x;
data.tileIndex = _OtherLightShadowData[lightIndex].y;
data.shadowMaskChannel = _OtherLightShadowData[lightIndex].w;
return data;
}

5. 最后实现Shadows.hlsl的GetOtherShadow方法计算采样阴影图集,其工作原理和GetCascadedShadow方法类似,不过它没有级联混合,且它是透视投影,需要将变换后的顶点位置的XYZ分量除以W做透视除法。此外我们还没有实现法线偏差,因此先乘以0。

//得到非定向光的阴影强度
float GetOtherShadow (OtherShadowData other, ShadowData global, Surface surfaceWS)
{
float3 normalBias = surfaceWS.interpolatedNormal * 0.0;
float4 positionSTS = mul(_OtherShadowMatrices[other.tileIndex],float4(surfaceWS.position + normalBias, 1.0));
//透视投影,变换位置的XYZ除以Z
return FilterOtherShadow(positionSTS.xyz / positionSTS.w);
}

loading

9.1.7 法线偏差

聚光灯也会受到阴影渗漏(Shadow Acne)的影响,由于透视投影的纹素大小是不固定的,所以阴影痤疮也是不固定的,离光源越远痤疮越大。

纹素大小随着与灯光平面的距离呈线性增加,灯光平面将世界分散在光线的前面或后面。因此我们可以计算纹素大小,从而计算距离1处的法线偏差,并将其发送到着色器,在那里我们将其缩放到适当的大小。在世界空间中,距离光平面为1的阴影图块的大小是聚光弧度半角的切线的两倍。下图是世界空间下图块大小的由来。

loading

1. 这与透视投影匹配,因此距离为1时的世界空间纹素大小等于2除以投影比例,为此我们使用其矩阵的左上角值。可以像方向光一样用它来计算法线偏差,不同之处在于因为没有多个级联,我们可以立即将光的法线偏移考虑进去。在Shadows脚本的RenderSpotShadows方法中设置阴影转换矩阵之前进行此设置。

    void RenderSpotShadows(int index, int split, int tileSize)
{
...
//计算法线偏差
float texelSize = 2f / (tileSize * projectionMatrix.m00);
float filterSize = texelSize * ((float)settings.other.filter + 1f);
float bias = light.normalBias * filterSize * 1.4142136f;

otherShadowMatrices[index] = ConvertToAtlasMatrix(projectionMatrix * viewMatrix,SetTileViewport(index, split, tileSize), split);
...
}

2. 然后将法线偏差发送到GPU。稍后我们需要向每个图块发送更多数据,先定义一个非定向光阴影图块的着色器标识ID和阴影图块数组,并在RenderOtherShadows方法中发送数据。

    static int otherShadowTilesId = Shader.PropertyToID("_OtherShadowTiles");
static Vector4[] otherShadowTiles = new Vector4[maxShadowedOtherLightCount];
void RenderOtherShadows()
{
...
buffer.SetGlobalMatrixArray(otherShadowMatricesId, otherShadowMatrices);
buffer.SetGlobalVectorArray(otherShadowTilesId, otherShadowTiles);
...
}

3. 定义一个SetOtherTileData方法,将法线偏差存储到向量的W分量中,然后将该向量存储到阴影图块数据数组中。

    //存储非定向光阴影图块数据
void SetOtherTileData(int index, float bias)
{
Vector4 data = Vector4.zero;
data.w = bias;
otherShadowTiles[index] = data;
}

4. 在RenderSpotShadows方法中计算好法线偏差后传递给该方法。

  float bias = light.normalBias * filterSize * 1.4142136f;
SetOtherTileData(index, bias);

5. 在Shadows.hlsl的_CustomShadows缓冲区中添加声明该阴影图块数据数组,然后在GetOtherShadow方法中根据图块索引获取对应的阴影图块数据,然后获取其中的法线偏差数据进行计算。

CBUFFER_START(_CustomShadows)
...
float4x4 _OtherShadowMatrices[MAX_SHADOWED_OTHER_LIGHT_COUNT];
float4 _OtherShadowTiles[MAX_SHADOWED_OTHER_LIGHT_COUNT];
CBUFFER_END

float GetOtherShadow (OtherShadowData other, ShadowData global, Surface surfaceWS)
{
float4 tileData = _OtherShadowTiles[other.tileIndex];
float3 normalBias = surfaceWS.interpolatedNormal * tileData.w;
...
}

loading

6. 现在我们有了一个法线偏差,但只有在固定距离才是正常的,为了根据与光平面的距离进行缩放,我们需要知道世界空间中光源位置和聚光灯方向,所以将这两个属性添加到OtherShadowData结构体中。

struct OtherShadowData 
{
...
float3 lightPositionWS;
float3 spotDirectionWS;
};

7. 由于这两个属性的数据来自光源本身而不是阴影数据,因此在Light.hlsl的GetOtherShadowData方法中将这两个属性值设为0,而在GetOtherLight方法中进行赋值。

OtherShadowData GetOtherShadowData(int lightIndex) 
{
...
data.lightPositionWS = 0.0;
data.spotDirectionWS = 0.0;
return data;
}

Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
Light light;
light.color = _OtherLightColors[index].rgb;
float3 position = _OtherLightPositions[index].xyz;
float3 ray = position - surfaceWS.position;
...
float3 spotDirection = _OtherLightDirections[index].xyz;
//计算聚光灯衰减值
float spotAttenuation = Square(saturate(dot(spotDirection, light.direction) * spotAngles.x + spotAngles.y));
OtherShadowData otherShadowData = GetOtherShadowData(index);
otherShadowData.lightPositionWS = position;
otherShadowData.spotDirectionWS = spotDirection;
...
}

8. 最后在Shadows.hlsl的GetOtherShadow方法中通过表面到光线方向和聚光灯方向的点积来找到与光平面的距离,并用它来缩放法线偏差。

float GetOtherShadow (OtherShadowData other, ShadowData global, Surface surfaceWS)
{
float4 tileData = _OtherShadowTiles[other.tileIndex];
float3 surfaceToLight = other.lightPositionWS - surfaceWS.position;
float distanceToLightPlane = dot(surfaceToLight, other.spotDirectionWS);
float3 normalBias = surfaceWS.interpolatedNormal * (distanceToLightPlane * tileData.w);
...
}

9.1.8 Clamped 采样

我们对方向光阴影配置了级联包围球,确保不会在适合的阴影图块之外采样,但不能对非定向光阴影使用相同的方法。聚光灯的阴影图块与圆锥体紧密相连,因此法线偏差和滤波尺寸会将采样推到圆椎体边界或之外,导致边缘附近有错误的图块产生的阴影。

loading

最简单的办法是手动Clamp采样让其限制在图块范围内,就像每个图块都是自己的单独纹理一样。这仍然会拉伸边缘附近的阴影,但不会引入无效阴影。

1. 在SetOtherTileData方法中添加偏移量和缩放比例的传参来计算和存储图块边界,图块最小纹理坐标是缩放的偏移量,将其存储在图块数据的XY分量中,由于图块是正方形,可以将缩放比例存储在Z分量,还需要在两个分量上将边界缩小半个纹素确保采样不会超出边界。

    void SetOtherTileData(int index, Vector2 offset, float scale, float bias)
{
float border = atlasSizes.w * 0.5f;
Vector4 data;
data.x = offset.x * scale + border;
data.y = offset.y * scale + border;
data.z = scale - border - border;
data.w = bias;
otherShadowTiles[index] = data;
}

2. 在Shadows.cs的RenderSpotShadows方法中通过SetTileViewport方法得到偏移量,然后将split的倒数作为缩放比例传入SetOtherTileData方法中。

    void RenderSpotShadows(int index, int split, int tileSize)
{
...
float bias = light.normalBias * filterSize * 1.4142136f;
Vector2 offset = SetTileViewport(index, split, tileSize);
SetOtherTileData(index, offset, 1f / split, bias);
otherShadowMatrices[index] = ConvertToAtlasMatrix(projectionMatrix * viewMatrix, offset, split);
...
}

3. ConverToAtlasMatrix方法中也使用了split的倒数作为缩放比例,所以只需计算一次然后传递给要使用它的两个方法,而ConvertToAtlasMatrix方法不必计算缩放比例。RenderDirectionalShadows方法中也需要计算一次缩放比例。

   void RenderSpotShadows(int index, int split, int tileSize)
{
...
float tileScale = 1f / split;
SetOtherTileData(index, offset, tileScale, bias);
otherShadowMatrices[index] = ConvertToAtlasMatrix(projectionMatrix * viewMatrix, offset, tileScale);
...
}

Matrix4x4 ConvertToAtlasMatrix(Matrix4x4 m, Vector2 offset, float scale)
{
...
//float scale = 1f / split;
...
}

void RenderDirectionalShadows(int index, int split, int tileSize)
{
...
float tileScale = 1f / split;
for (int i=0;i<cascadeCount;i++)
{
...
dirShadowMatrices[tileIndex] = ConvertToAtlasMatrix(projectionMatrix * viewMatrix,SetTileViewport(tileIndex, split, tileSize), tileScale);
...
}

}

4. 要应用边界,需要在Shadows.hlsl的SampleOtherShadowAtlas方法中添加一个float3类型的参数,使用它限制阴影图块空间中的位置。FilterThertherShadows也要添加这个参数,通过GetOtherShadow方法来传递该值。

float SampleOtherShadowAtlas (float3 positionSTS,float3 bounds) 
{
positionSTS.xy = clamp(positionSTS.xy, bounds.xy, bounds.xy + bounds.z);
return SAMPLE_TEXTURE2D_SHADOW(_OtherShadowAtlas, SHADOW_SAMPLER, positionSTS);
}
float FilterOtherShadow (float3 positionSTS, float3 bounds)
{
...
for (int i = 0; i < OTHER_FILTER_SAMPLES; i++)
{
//遍历所有样本得到权重和
shadow += weights[i] * SampleOtherShadowAtlas(float3(positions[i].xy, positionSTS.z), bounds);
}
return shadow;
#else
return SampleOtherShadowAtlas(positionSTS, bounds);
#endif
}

float GetOtherShadow (OtherShadowData other, ShadowData global, Surface surfaceWS)
{
...
//透视投影,变换后的顶点位置的XYZ除以Z
return FilterOtherShadow(positionSTS.xyz / positionSTS.w,tileData.xyz);
}

现在不会再有来自错误的图块中的阴影了。

loading


9.2 点光源阴影

点光源的阴影和聚光灯类似,不同的是点光源不局限于圆锥体。点光源生成的阴影,其阴影深度贴图存储在一个立方体纹理中,然后单独渲染立方体的六个面的阴影。可以将点光源视为6个灯光,它会占据阴影图集6个图块,这意味着我们目前可以支持最多两个点光源的实时阴影,它会占据最大值16个图块中的12个,如果少于6个图块空间则点光源是无法渲染实时阴影的。

9.2.1 渲染点光源阴影

1. 在Shadows脚本的ShadowedOtherLight结构体中添加一个bool属性来判断我们渲染阴影时当前是否处理的是点光源。

    struct ShadowedOtherLight
{
...
public bool isPoint;
}

2. 在ReserveOtherShadows方法中检测该光源类型是否是点光源,如果是,则灯光计数应该加6个,如果超过了最大灯光数量则多出的非定向光源只能有烘焙阴影,如果图集有足够的空间还需要在阴影数据的Z分量存储是否为点光源的标记,以便在着色器中检测点光源。

public Vector4 ReserveOtherShadows(Light light, int visibleLightIndex)
{
...
bool isPoint = light.type == LightType.Point;
int newLightCount = shadowedOtherLightCount + (isPoint ? 6 : 1);
//非定向光源数量是否超过了设置的最大值或者是否没有阴影需要渲染
if (newLightCount >= maxShadowedOtherLightCount ||!cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b) )
{
return new Vector4(-light.shadowStrength, 0f, 0f, maskChannel);
}

shadowedOtherLights[shadowedOtherLightCount] = new ShadowedOtherLight
{
visibleLightIndex = visibleLightIndex,
slopeScaleBias = light.shadowBias,
normalBias = light.shadowNormalBias,
isPoint = isPoint
};
Vector4 data = new Vector4(light.shadowStrength, shadowedOtherLightCount,isPoint ? 1f : 0f, maskChannel);
shadowedOtherLightCount = newLightCount;
return data;
}

3. 修改RenderOtherShadows方法,在遍历非定向光源时,判断光源类型是否为点光源,如果是则调用新的RenderPointShadows方法渲染点光源阴影,且光源数量技术增加6。

for (int i = 0; i < shadowedOtherLightCount;)
{
if (shadowedOtherLights[i].isPoint)
{
RenderPointShadows(i, split, tileSize);
i += 6;
}
else
{
RenderSpotShadows(i, split, tileSize);
i += 1;
}
}

4. 复制RenderSpotShadows方法,命名为RenderPointShadows,这里需要做一些修改。首先它需要渲染6次,循环遍历6个阴影图块。然后剔除结果需要调用ComputePointShadowMatricesAndCullingPrimitives方法,此方法在可见光索引之后还需要两个额外参数,就是立方体的面索引和一个偏差值,我们为立方体的每个面都渲染一次,偏差值目前设为0。

立方体的面的视野始终为90°,因此距离1处的世界空间图块尺寸始终为2。这意味着我们可以将法线偏差的计算从循环中移出来,也可以用图块缩放比例做到这一点。

void RenderPointShadows(int index, int split, int tileSize)
{
ShadowedOtherLight light = shadowedOtherLights[index];
var shadowSettings = new ShadowDrawingSettings(cullingResults, light.visibleLightIndex);
float texelSize = 2f / tileSize;
float filterSize = texelSize * ((float)settings.other.filter + 1f);
//计算法线偏差
float bias = light.normalBias * filterSize * 1.4142136f;
float tileScale = 1f / split;

for (int i = 0; i < 6; i++)
{
cullingResults.ComputePointShadowMatricesAndCullingPrimitives(light.visibleLightIndex, (CubemapFace)i, 0f,out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,out ShadowSplitData splitData);
shadowSettings.splitData = splitData;
int tileIndex = index + i;
Vector2 offset = SetTileViewport(tileIndex, split, tileSize);
SetOtherTileData(tileIndex, offset, tileScale, bias);
otherShadowMatrices[tileIndex] = ConvertToAtlasMatrix(projectionMatrix * viewMatrix, offset, tileScale);
//设置视图投影矩阵
buffer.SetViewProjectionMatrices(viewMatrix, projectionMatrix);
//设置斜度比例偏差值
buffer.SetGlobalDepthBias(0f, light.slopeScaleBias);
//绘制阴影
ExecuteBuffer();
context.DrawShadows(ref shadowSettings);
buffer.SetGlobalDepthBias(0f, 0f);
}

}

9.2.2 采样点光源阴影

1. 因为点光源的阴影贴图存储在一个立方体纹理中,然后在着色器中对其采样。但现在我们是将立方体纹理的6个面作为阴影图块存储在图集中的,因此我们无法使用标准的立方体纹理采样获取阴影数据。由于我们要自己确定合适的面来进行采样,所以需要知道是否在处理点光源,以及物体表面到光源的方向,为此将这两个属性添加到OtherShadowData结构体中。

struct OtherShadowData 
{
...
bool isPoint;
float3 lightDirectionWS;
};

2. 在GetOtherShadowData方法中为这两个属性赋值,判断非定向光阴影数据的Z分量是否为1,如果是则为点光源,然后获取光的方向。

OtherShadowData GetOtherShadowData(int lightIndex) 
{
...
data.isPoint = _OtherLightShadowData[lightIndex].z == 1.0;
data.lightDirectionWS = 0.0;
return data;
}

Light GetOtherLight (int index, Surface surfaceWS, ShadowData shadowData)
{
...
otherShadowData.lightDirectionWS = light.direction;
otherShadowData.spotDirectionWS = spotDirection;
...
}

3. 接下来在GetOtherShadow方法中调整图块索引和光平面,首先将它们转换为变量,这些变量最初是为聚光灯配置的。如果有点光源,我们必须使用适当的轴对齐平面来代替。我们可以使用CubeMapFaceID方法,通过传递光的反方向来查找面偏移,该方法是源码库中定义的。立方体纹理面的顺序是 +X、-X、+Y、-Y、+Z、-Z,这与我们渲染它们的方法相匹配,然后将得到的面偏移添加到图块索引中。

float GetOtherShadow (OtherShadowData other, ShadowData global, Surface surfaceWS)
{
float tileIndex = other.tileIndex;
float3 lightPlane = other.spotDirectionWS;
if (other.isPoint)
{
float faceOffset = CubeMapFaceID(-other.lightDirectionWS);
tileIndex += faceOffset;
}
float4 tileData = _OtherShadowTiles[tileIndex];
float3 surfaceToLight = other.lightPositionWS - surfaceWS.position;
float distanceToLightPlane = dot(surfaceToLight, lightPlane);
float3 normalBias = surfaceWS.interpolatedNormal * (distanceToLightPlane * tileData.w);
float4 positionSTS = mul(_OtherShadowMatrices[tileIndex],float4(surfaceWS.position + normalBias, 1.0));
return FilterOtherShadow(positionSTS.xyz / positionSTS.w,tileData.xyz);
}

4. 下面要使用与面方向相匹配的光平面,先创建一个静态数组,并使用面偏移来索引它,平面法线方向必须是指向面的方向的反方向,就像聚光灯方向是由表面指向聚光灯一样。

static const float3 pointShadowPlanes[6] = 
{
float3(-1.0, 0.0, 0.0),
float3(1.0, 0.0, 0.0),
float3(0.0, -1.0, 0.0),
float3(0.0, 1.0, 0.0),
float3(0.0, 0.0, -1.0),
float3(0.0, 0.0, 1.0)
};
float GetOtherShadow (OtherShadowData other, ShadowData global, Surface surfaceWS)
{
...
if (other.isPoint)
{
float faceOffset = CubeMapFaceID(-other.lightDirectionWS);
tileIndex += faceOffset;
lightPlane = pointShadowPlanes[faceOffset];
}
...
}

loading

现在可以看到点光源的实时阴影被渲染出来了,即使没有法线偏差也基本没有阴影痤疮。不幸的是,光线会通过物体漏到离它们非常近的表面上,增加阴影偏差可能更糟,似乎还会在靠近其它表面的物体的阴影中切孔。

这是因为Unity为点光源渲染阴影的方式。Unity把它们颠倒过来,从而扭转了三角形的缠绕顺序。通常从光的角度来看,正面是绘制的,但现在背面也被渲染出来了,这可以防止大多数痤疮,但会引发漏光。我们不能停止翻转,但可以通过ComputePointShadowMatricesAndCullingPrimitives方法中得到的视图矩阵,对其第二行进行取反来撤销翻转,这在第二次将阴影图集中的所有内容颠倒过来,使一切恢复正常。因为该行的第一个分量始终为0,所以只需要将其它三个分量取反。

注意,将MeshRenderer的投影模式设置为双面的对象不会受到影响,因为它们的面都不会被剔除。

cullingResults.ComputePointShadowMatricesAndCullingPrimitives(light.visibleLightIndex, (CubemapFace)i, 0f,out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,out ShadowSplitData splitData);
viewMatrix.m11 = -viewMatrix.m11;
viewMatrix.m12 = -viewMatrix.m12;
viewMatrix.m13 = -viewMatrix.m13;

9.2.3 视场偏差(Field of View Bias)

立方体纹理的面之间总归是不连续的,因为纹理平面的方向会突然变化 90°。常规立方体纹理采样可以在某种方式隐藏它,因为它可以在面之间进行插值,但我们是从单个图块逐片元进行采样,所以会存在和聚光灯阴影图块边缘一样的问题,但它们并没有隐藏,因为没有聚光衰减。

loading

当渲染阴影时我们可以通过增加视场(Field of View,FOV)偏差来减少这些伪影,这样就不会在图块边缘之外采样,这就是ComputePointShadowMatricesAndCullingPrimitives的偏差参数的用法。我们在距离光源为1处将图块大小设置超过2,具体来说,是将法线偏差加在每一侧的滤波尺寸上,FOV角度的一半的切线值等于1+偏差值+滤波尺寸,然后将其加倍并转换为度数,最后减去90°,我们将其用于RenderPointShadows方法中的FOV偏差。

void RenderPointShadows(int index, int split, int tileSize)
{
...
float fovBias = Mathf.Atan(1f + bias + filterSize) * Mathf.Rad2Deg * 2f - 90f;
for (int i = 0; i < 6; i++)
{
cullingResults.ComputePointShadowMatricesAndCullingPrimitives(light.visibleLightIndex, (CubemapFace)i, fovBias, out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,out ShadowSplitData splitData);
...
}

}

loading

上图是带有FOV偏差的效果,其实这种方法也并不完美,因为通过增加图块大小,纹素大小也会增加,因此滤波尺寸也增加,法线偏差也应增加,这意味着需要再次增加FOV。但差异通常不大,我们可以忽略图块大小的增加,除非将较大的法线偏差和滤波尺寸与一个小尺寸的阴影图集结合使用。

源代码及PDF课件地址:

文件下载
暂无评论发表评论